﻿using System;
using System.Timers;
using NUnit.Framework;

namespace TimeBoardTest
{
    /// <summary>
    /// The unit tests for playTimer implementation.
    /// </summary>
    [TestFixture]
    public class MatchTimerTest
    {
        private IPlayTimer playTimer;

        /// <summary>
        /// Sets up the test method.
        /// </summary>
        [SetUp]
        public void SetUp()
        {
            var matchDuration = new TimeSpan(1, 0, 0);
            const int periodsCount = 3;
            this.playTimer = new PlayTimer(matchDuration, periodsCount);
        }

        /// <summary>
        /// Tears down the test method.
        /// </summary>
        [TearDown]
        public void TearDown()
        {
            this.playTimer = null;
        }

        /// <summary>
        /// Test API: Constructor
        /// Test Scenario: Happy Path
        /// Expected Behavior: Match Duration is set from constructor parameter.
        /// </summary>
        [Test]
        public void Constructor_HappyPath_TotalMatchDurationIsSet()
        {
            // test preparation
            var matchDuration = new TimeSpan(0, 0, 60, 0);
            const int periodsCount = 3;
            // test exception
            this.playTimer = new PlayTimer(matchDuration, periodsCount);
            // test verification
            Assert.AreEqual(matchDuration, playTimer.MatchDuration,
                "TotalMatchDuration is not set.");
        }

        /// <summary>
        /// Test API: Constructor
        /// Test Scenario: Happy Path
        /// Expected Behavior: Periods Count is set from constructor parameter.
        /// </summary>
        [Test]
        public void Constructor_HappyPath_PeriodsCountIsSet()
        {
            // test preparation
            var totalMatchDuration = new TimeSpan(0, 0, 60, 0);
            const int periodsCount = 3;
            // test execution
            this.playTimer = new PlayTimer(totalMatchDuration, periodsCount);
            // test verification
            Assert.AreEqual(periodsCount, playTimer.PeriodsCount,
                "PeriodsCount is not set.");
        }

        /// <summary>
        /// Test API: Constructor
        /// Test Scenario: MatchDuration is zero.
        /// Expected Behavior: NotSupportedException is thrown.
        /// </summary>
        [Test]
        [ExpectedException(typeof(NotSupportedException))]
        public void Constructor_TotalMatchDurationIsZero_NotSupportedException()
        {
            // test preparation
            var matchDuration = new TimeSpan(0, 0, 0, 0);
            const int periodsCount = 3;
            // test execution
            new PlayTimer(matchDuration, periodsCount);
        }

        /// <summary>
        /// Test API: Constructor
        /// Test Scenario: PeriodsCount is less then one.
        /// Expected Behavior: NotSupportedException is thrown.
        /// </summary>
        [Test]
        [ExpectedException(typeof(NotSupportedException))]
        public void Constructor_PeriodsCountIsLessThenOne_NotSupportedException()
        {
            // test preparation
            var matchDuration = new TimeSpan(0, 0, 60, 0);
            const int periodsCount = 0;
            // test execution
            new PlayTimer(matchDuration, periodsCount);
        }

        /// <summary>
        /// Test API: Start
        /// Test Scenario: HappyPath
        /// Expected Behavior: PlayTime is changed.
        /// </summary>
        [Test]
        public void Start_HappyPath_PlayTimeIsChanged()
        {
            // test preparation
            var matchDuration = new TimeSpan(1, 0, 0);
            const int periodsCount = 3;
            this.playTimer = new PlayTimer(matchDuration, periodsCount);
            TimeSpan playTime = this.playTimer.PlayTime;
            // test execution
            this.playTimer.Start();
            System.Threading.Thread.Sleep(1000);
            // test verification
            Assert.AreNotEqual(playTime, this.playTimer.PlayTime,
                "PlayTime is not changed.");
        }

        /// <summary>
        /// Test API: Stop
        /// Test Scenario: HappyPath
        /// Expected Behavior: PlayTime is changed.
        /// </summary>
        [Test]
        public void Stop_HappyPath_PlayTimeIsNotChanged()
        {
            // test execution
            this.playTimer.Start();
            TimeSpan playTime = this.playTimer.PlayTime;
            this.playTimer.Stop();
            System.Threading.Thread.Sleep(1000);
            // test verification
            Assert.AreEqual(playTime, this.playTimer.PlayTime,
                "PlayTime is not changed.");
        }

        /// <summary>
        /// Test API: IncreasePlayTimeBySecond
        /// Test Scenario: Timer is stopped
        /// Expected Behavior: PlayTime is increased by second
        /// </summary>
        [Test]
        public void IncreasePlayTimeBySecond_TimerIsStopped_PlayTimeIsIncreasedBySecond()
        {
            // test preparation
            var increasedPlayTime = new TimeSpan(0, 0, 1);
            this.playTimer.Stop();
            // test execution
            this.playTimer.IncreasePlayTimeBySecond();
            // test verification
            Assert.AreEqual(increasedPlayTime, this.playTimer.PlayTime,
                "PlayTime is not increased by one second.");
        }

        /// <summary>
        /// Test API: IncreasePlayTimeBySecond
        /// Test Scenario: Timer is started.
        /// Expected Behavior: PlayTime is not changed.
        /// </summary>
        [Test]
        public void IncreasePlayTimeBySecond_TimerIsStarted_PlayTimeIsNotChanged()
        {
            // test preparation
            this.playTimer.Start();
            // test execution
            TimeSpan playTimeBefore = this.playTimer.PlayTime;
            this.playTimer.IncreasePlayTimeBySecond();
            // test verification
            Assert.AreEqual(playTimeBefore, this.playTimer.PlayTime,
                "PlayTime is changed. Should be same when timer is started while calling IncreasePlayTimeBySecond.");
        }

        /// <summary>
        /// Test API: DecreasePlayTimeBySecond
        /// Test Scenario: Timer is stopped.
        /// Expected Behavior: PlayTime is decreased by second.
        /// </summary>
        [Test]
        public void DecreasePlayTimeBySecond_TimerIsStopped_PlayTimeIsIncreasedBySecond()
        {
            // test preparation
            var decreasedPlayTime = this.playTimer.PlayTime;
            this.playTimer.Stop();
            this.playTimer.IncreasePlayTimeBySecond();
            // test execution
            this.playTimer.DecreasePlayTimeBySecond();
            // test verification
            Assert.AreEqual(decreasedPlayTime, this.playTimer.PlayTime,
                "PlayTime is not decreased by one second.");
        }

        /// <summary>
        /// Test API: DecreasePlayTimeBySecond
        /// Test Scenario: Timer is started.
        /// Expected Behavior: PlayTime is not changed.
        /// </summary>
        [Test]
        public void DecreasePlayTimeBySecond_TimerIsStarted_PlayTimeIsNotChanged()
        {
            // test preparation
            this.playTimer.Start();
            // test execution
            TimeSpan playTimeBefore = this.playTimer.PlayTime;
            this.playTimer.DecreasePlayTimeBySecond();
            // test verification
            Assert.AreEqual(playTimeBefore, this.playTimer.PlayTime,
                "PlayTime is changed. Should be same when timer is started while calling DecreasePlayTimeBySecond.");
        }

        /// <summary>
        /// Test API: DecreasePlayTimeBySecond
        /// Test Scenario: PlayTime is zero
        /// Expected Behavior: PlayTime is not decreased
        /// </summary>
        [Test]
        public void DecreasePlayTimeBySecond_PlayTimeIsZero_PlayTimeIsNotDecreased()
        {
            // test preparation
            this.playTimer.Stop();
            // test execution
            this.playTimer.DecreasePlayTimeBySecond();
            // test verification
            Assert.IsTrue(TimeSpan.Zero <= this.playTimer.PlayTime,
                "PlayTime is less then zero.");
        }
    }

    /// <summary>
    /// The match timer interface responsible for:
    /// Get the match duration;
    /// Get the periods count;
    /// </summary>
    public interface IPlayTimer
    {
        /// <summary>
        /// Gets the duration of the match.
        /// </summary>
        /// <value>
        /// The duration of the match.
        /// </value>
        TimeSpan MatchDuration { get; }

        /// <summary>
        /// Gets the periods count.
        /// </summary>
        /// <value>
        /// The periods count.
        /// </value>
        int PeriodsCount { get; }

        /// <summary>
        /// Gets the play time.
        /// </summary>
        /// <value>
        /// The play time.
        /// </value>
        TimeSpan PlayTime { get; }

        /// <summary>
        /// Starts this instance.
        /// </summary>
        void Start();

        /// <summary>
        /// Stops this instance.
        /// </summary>
        void Stop();

        /// <summary>
        /// Increases the play time by second.
        /// </summary>
        void IncreasePlayTimeBySecond();

        /// <summary>
        /// Decreases the play time by second.
        /// </summary>
        void DecreasePlayTimeBySecond();
    }

    /// <summary>
    /// The default implementation of IPlayTimer.
    /// </summary>
    public class PlayTimer : IPlayTimer
    {
        private const int TIMER_INTERVAL = 100;

        private readonly Timer timer;

        private readonly TimeSpan second = new TimeSpan(0, 0, 1);

        /// <summary>
        /// Initializes a new instance of the <see cref="PlayTimer" /> class.
        /// </summary>
        /// <param name="matchDurationIn">The match duration.</param>
        /// <param name="periodsCountIn">The periods count.</param>
        public PlayTimer(TimeSpan matchDurationIn, int periodsCountIn)
        {
            if (periodsCountIn < 1)
            {
                throw new NotSupportedException("Count of periods must be higher or equal to 1.");
            }
            if (matchDurationIn < new TimeSpan(0, 1, 0))
            {
                throw new NotSupportedException("Total time duration must be higher or equal to 1 minute.");
            }
            MatchDuration = matchDurationIn;
            PeriodsCount = periodsCountIn;
            this.timer = new Timer(TIMER_INTERVAL);
            this.timer.Elapsed += TimerElapsed;
        }

        /// <summary>
        /// Gets the duration of the match.
        /// </summary>
        /// <value>
        /// The duration of the match.
        /// </value>
        public TimeSpan MatchDuration { get; private set; }

        /// <summary>
        /// Gets the periods count.
        /// </summary>
        /// <value>
        /// The periods count.
        /// </value>
        public int PeriodsCount { get; private set; }

        /// <summary>
        /// Gets the play time.
        /// </summary>
        /// <value>
        /// The play time.
        /// </value>
        public TimeSpan PlayTime { get; private set; }

        /// <summary>
        /// Starts this instance.
        /// </summary>
        public void Start()
        {
            this.timer.Start();
        }

        /// <summary>
        /// Stops this instance.
        /// </summary>
        public void Stop()
        {
            this.timer.Stop();
        }

        /// <summary>
        /// Increases the play time by second.
        /// </summary>
        public void IncreasePlayTimeBySecond()
        {
            if (!timer.Enabled)
            {
                PlayTime = PlayTime.Add(second);
            }
        }

        /// <summary>
        /// Decreases the play time by second.
        /// </summary>
        public void DecreasePlayTimeBySecond()
        {
            if (!timer.Enabled)
            {
                if (PlayTime - second >= TimeSpan.Zero)
                {
                    PlayTime = PlayTime.Add(-second);
                }
            }
        }

        /// <summary>
        /// Timers the elapsed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="ElapsedEventArgs" /> instance containing the event data.</param>
        private void TimerElapsed(object sender, ElapsedEventArgs e)
        {
            PlayTime = PlayTime.Add(new TimeSpan(0, 0, 0, 0, TIMER_INTERVAL));
        }
    }
}
